#import "Dock.h"

// NSObject wrapper for CGPoint implementation
@implementation Pair

@synthesize point;

+ (id) MakePair:(CGPoint)point
{
	return [[[self alloc] initPair:point] autorelease];
}

- (id) initPair:(CGPoint) cgPoint
{
	if ((self = [super init]))
	{
		self.point = cgPoint;
	}
	return self;
}

@end

@interface Dock (Private)
// returns touched menu item, if any
-(ButtonItem *) itemForTouch: (UITouch *) touch;
@end

@implementation Dock

+ (id) dockWithPosition:(CGPoint)pos
{
	return [[[self alloc] initWithPosition:pos] autorelease];
}

- (id) initWithPosition:(CGPoint)pos
{
	if( (self=[super init]) ) {
		
		self.isTouchEnabled = YES;
	
		CGSize s = [[Director sharedDirector] winSize];
		
		self.relativeAnchorPoint = NO;
		anchorPoint_ = ccp(0.5f, 0.5f);
		[self setContentSize:s];
		
		// IS this stuff below actually needed?
		
		// XXX: in v0.7, winSize should return the visible size
		// XXX: so the bar calculation should be done there
		CGRect r = [[UIApplication sharedApplication] statusBarFrame];
		ccDeviceOrientation orientation = [[Director sharedDirector] deviceOrientation];
		if( orientation == CCDeviceOrientationLandscapeLeft || orientation == CCDeviceOrientationLandscapeRight )
			s.height -= r.size.width;
		else
			s.height -= r.size.height;
		
		//self.position = ccp(s.width/2, s.height/2);
		
		self.position = pos;
		
		// Initialize the arrays
		itemsArray = [[NSMutableArray alloc] initWithCapacity: 6];
		positionsArray = [[NSMutableArray alloc] initWithCapacity: 6];
		
		selectedItem = nil;
		state = kMenuStateWaiting;
		
		contracted = NO;
		
		// Set up a scheduler to move the buttons
		[self schedule:@selector(Update) interval: (1.0f/60.0f)];	
	}
	
	return self;
}

+(id) menuWithItems: (MenuItem*) item, ...
{
	va_list args;
	va_start(args,item);
	
	id s = [[[self alloc] initWithItems: item vaList:args] autorelease];
	
	va_end(args);
	return s;
}

/*
 * override add:
 */
-(id) addChild:(ButtonItem*)child z:(int)z tag:(int) aTag
{
	NSAssert( [child isKindOfClass:[ButtonItem class]], @"Menu only supports ButtonItem objects as children");
	
	// Position the item based on where it is in the dock and how large the icons are
	child.position = CGPointMake(itemsArray.count * (child.contentSize.width + DOCK_SEPERATION), 0);
	// Set the desired position to be the same as the actual position to keep it from moving
	child.desiredPos = child.position;
	
	// Make a pair representation of the coordinates to add to the positions array
	Pair *pair = [Pair MakePair:child.position];
	
	// Add it to the array
	[itemsArray addObject:child];
	[positionsArray addObject:pair];
	
	// Add it as a child to the dock object
	return [super addChild:child z:z tag:aTag];
}

#pragma mark Menu - Events

-(void) registerWithTouchDispatcher
{
	[[TouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:INT_MIN+1 swallowsTouches:YES];
}

-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
	//NSLog(@"Dock: Touch Began, state: %d", state);
	if( state != kDockStateWaiting ) return NO;
	
	selectedItem = [self itemForTouch:touch];
	//[selectedItem selected];
	
	// Only allow selection if an item is actually selected and as long as it isn't moving
	if (selectedItem && selectedItem.buttonMoveState == kLocked) 
	{
		[selectedItem selected];		
		state = kDockStateTrackingTouch;
		//NSLog(@"Inside touch began, change state to: %d", state);		
		// Store where the item started moving from for boundary checks later (store the absolute position)
		startPoint = ccpAdd(selectedItem.position, self.position);
		return YES;
	}
	return NO;
}

-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
	//NSLog(@"Dock: Touch Ended, state: %d", state);	
	NSAssert(state == kDockStateTrackingTouch, @"[Menu ccTouchEnded] -- invalid state");
	
	[selectedItem unselected];
	[selectedItem activate];
	
	state = kDockStateWaiting;
	
	// We are outside the top or bottom boundary and user has let go of the item
	// Destroy it
	if (boundaryState == kTopBoundary)
	{	
		[self removeChild:selectedItem cleanup:YES];
		// Reset the flag for next time
		contracted = NO;		
	}
	// No boundary crossings, go back to original position	
	else
	{
		selectedItem.position = ccpSub(startPoint, self.position);
	}
}

-(void) ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event
{
	//NSLog(@"Dock: Touch Cancelled");
	
	NSAssert(state == kDockStateTrackingTouch, @"[Menu ccTouchCancelled] -- invalid state");
	
	[selectedItem unselected];
	
	state = kDockStateWaiting;
}

-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
	NSAssert(state == kDockStateTrackingTouch, @"[Menu ccTouchMoved] -- invalid state");
	
	CGPoint touchLocation = [touch locationInView: [touch view]];
	touchLocation = [[Director sharedDirector] convertToGL: touchLocation];
	
#ifdef DEBUG_DOCK
	NSLog(@"Dock location: %4.2f, %4.2f", self.position.x, self.position.y);
	NSLog(@"Item location: %4.2f, %4.2f", selectedItem.position.x, selectedItem.position.y);
	NSLog(@"Current touch: %4.2f, %4.2f", touchLocation.x, touchLocation.y);
#endif
	 
	// Find the new position relative to the dock item
	// This is touch (absolute) - dock (absolute)
	CGPoint location = ccpSub(touchLocation, self.position);
	
	// Keep the item following the cursor
	selectedItem.position = location;
	
	// Check if item has gone past a boundary
	boundaryState = [self BoundaryCheck:touchLocation];
	
	// Get the index of the selected item in the array
	int index = [itemsArray indexOfObject:selectedItem];
	
	// Deal with boundary crossings
	switch (boundaryState)
	{
		case kRightBoundary:
			// Check that there is an item to our right
			if (index + 1 < itemsArray.count)
			{
				
				// Get a reference to the item on our right
				ButtonItem *temp = [itemsArray objectAtIndex:index + 1];
				// Move the item whose territory you're in into your old spot				
				temp.desiredPos = [[positionsArray objectAtIndex:index] point];
				// Unlock the button so that it can move
				temp.buttonMoveState = kMovingLeft;
				
				// Swap positions in the array
				[itemsArray removeObjectAtIndex:index];
				[itemsArray insertObject:selectedItem atIndex:index + 1];
						
				// Set the target's old location as this item's base
				selectedItem.desiredPos = [[positionsArray objectAtIndex:index + 1] point];
				startPoint = ccpAdd(self.position, selectedItem.desiredPos);
			}
			// Else we are at an array end, do nothing
			else
			{
				boundaryState = kNoBoundary;
			}
			break;
		case kLeftBoundary:
			// Check that there is an item to our left
			if (index > 0)
			{
				// Get a reference to the item on our left
				ButtonItem *temp = [itemsArray objectAtIndex:index - 1];
				// Move the item whose territory you're in into your old spotx				
				temp.desiredPos = [[positionsArray objectAtIndex:index] point];
				// Unlock the button so that it can move
				temp.buttonMoveState = kMovingRight;
				
				// Swap positions in the array
				[itemsArray removeObjectAtIndex:index];
				[itemsArray insertObject:selectedItem atIndex:index - 1];
				
							
				// Set the target's old location as this item's base
				selectedItem.desiredPos = [[positionsArray objectAtIndex:index - 1] point];
				startPoint = ccpAdd(self.position, selectedItem.desiredPos);				
			}
			// Else we are at an array end, do nothing			
			else
			{
				boundaryState = kNoBoundary;
			}			
			break;
		// State where the item has been dragged past the top or the bottom of the dock
		case kTopBoundary:
			if (!contracted)
			{
				// Contract the dock
				[self Contract];
				// Set the flag to indicate the item is outside the dock
				contracted = YES;
			}
			break;
		// State where the selected item has been outside the dock and is being returned
		case kReturnToDock:
			// Determine the index to add it back to and then expand the existing items to make room
			[self Expand:[self CalculateInsertionIndex:location]];
			// Reset the flag 
			contracted = NO;
			break;
		case kNoBoundary:
		default:
			// Don't do anything
			break;
	}
}

// Determine what index in the dock the item is closest to, based on relative x-coordinate
- (int) CalculateInsertionIndex: (CGPoint)position
{
	int width = selectedItem.contentSize.width;
	int tick = width + DOCK_SEPERATION;
	
	// Figure out the leftmost boundary (midway through where the current first item in dock is)	
	CGFloat leftBound = [[itemsArray objectAtIndex:0] position].x + width / 2.0f;
	
	// This offset is necessary to normalize the left bound to the tick distance
	// so that we can use the tick as the divisor
	CGFloat offset = tick - leftBound;
	
	// Again, touch position must be adjusted by the same offset as above
	// Index is calculated by dividing to see which "bin" we are in
	return ((int)(position.x + offset))/tick;
}

// Function used when user moves an item out of the dock
- (void) Contract
{
	// Calculate the move distance
	CGPoint moveDistance = CGPointMake((DOCK_SEPERATION + selectedItem.contentSize.width) / 2.0f, 0); 		
	
	// Get the index of the selected item in the array
	int index = [itemsArray indexOfObject:selectedItem];	
	
	// Since we are contracting, remove the last entry from the position array
	// Shift all other positions inwards
	[positionsArray removeLastObject];
	for (Pair *pair in positionsArray)
	{
		pair.point = ccpAdd(pair.point, moveDistance);
	}
	
	// Shift all items on the left side to the right
	for (int i = 0; i < index; i++)
	{
		ButtonItem *item = [itemsArray objectAtIndex:i];
		item.desiredPos = [[positionsArray objectAtIndex:i] point];
		item.buttonMoveState = kMovingRight;
		
	}
	// Shift all items on the right side to the left
	for (int i = index + 1; i < itemsArray.count; i++)
	{
		ButtonItem *item = [itemsArray objectAtIndex:i];
		item.desiredPos = [[positionsArray objectAtIndex:i-1] point];
		item.buttonMoveState = kMovingLeft;
	}	
	
	// Remove the item from the array
	[itemsArray removeObjectAtIndex:index];	
}

// Function used when user moves an item back into the dock
- (void) Expand:(int)insertIndex
{
	// Calculate the move distance
	CGPoint moveDistance = CGPointMake((DOCK_SEPERATION + selectedItem.contentSize.width) / 2.0f, 0); 	
	
	// Add a new position to the end of the positions array since we are adding an object to the dock
	CGPoint last = [[positionsArray lastObject] point];
	Pair *pair = [Pair MakePair:CGPointMake(last.x + DOCK_SEPERATION + selectedItem.contentSize.width, 0)];
	[positionsArray addObject:pair];
	// Shift all other positions to the left
	for (Pair *pair in positionsArray)
	{
		pair.point = ccpSub(pair.point, moveDistance);
	}
	
	// Put the item that the user dragged back into the dock into the correct position
	selectedItem.desiredPos = [[positionsArray objectAtIndex:insertIndex] point];
	startPoint = ccpAdd(self.position, selectedItem.desiredPos);	
	
	// Add the item into the array at the given index
	[itemsArray insertObject:selectedItem atIndex:insertIndex];
	
	// Get the index of the selected item in the array
	int index = [itemsArray indexOfObject:selectedItem];	
	
	// Shift all items on the left side to the left
	for (int i = 0; i < index; i++)
	{
		ButtonItem *item = [itemsArray objectAtIndex:i];
		item.desiredPos = [[positionsArray objectAtIndex:i] point];
		item.buttonMoveState = kMovingLeft;
		
	}
	// Shift all items on the right side to the right
	for (int i = index + 1; i < itemsArray.count; i++)
	{
		ButtonItem *item = [itemsArray objectAtIndex:i];
		item.desiredPos = [[positionsArray objectAtIndex:i] point];		
		item.buttonMoveState = kMovingRight; 
	}	
}

// For moving the buttonItems
- (void) Update
{
	[children makeObjectsPerformSelector:@selector(Tick)];
}

// Takes in an absolute position
- (BoundaryState) BoundaryCheck: (CGPoint)position 
{
#ifdef DEBUG_DOCK
	NSLog(@"Original location: %4.2f, %4.2f", startPoint.x, startPoint.y);	
	NSLog(@"Content size: %4.2f, %4.2f", selectedItem.contentSize.width, selectedItem.contentSize.height);
	NSLog(@"Location to check: %4.2f, %4.2f", position.x, position.y);	
#endif
	
	CGSize size = selectedItem.contentSize;
	
	// ButtonItem locations are always reference from the bottom left
	// Check if we've crossed the top or bottom of the dock
	if (abs(startPoint.y - position.y) > size.height)
	{
		return kTopBoundary;
	}	
	// If not, we now need to determine whether we came from a state where the user had the 
	// item outside the dock or whether the item is just moving around within the dock
	else
	{
		if (contracted)
		{
			// This indicates that the item has been returned to the dock after being moved outside
			return kReturnToDock;
		}
		else
		{
			// Check the left boundary
			if (startPoint.x - position.x > size.width)
			{
				return kLeftBoundary;
			}
			// Check the right boundary
			if (position.x - startPoint.x > size.width)
			{
				return kRightBoundary;		
			}	
			// Else the item hasn't been moved past a boundary
			return kNoBoundary;
		}
	}
}

#pragma mark Menu - Private

- (ButtonItem *) itemForTouch: (UITouch *) touch;
{
	CGPoint touchLocation = [touch locationInView: [touch view]];
	touchLocation = [[Director sharedDirector] convertToGL: touchLocation];
	
	for( ButtonItem* item in children ) {
		CGPoint local = [item convertToNodeSpace:touchLocation];
		
		CGRect r = [item rect];
		r.origin = CGPointZero;
		
		if( CGRectContainsPoint( r, local ) )
			return item;
	}
	return nil;
}

- (void) dealloc
{
	[itemsArray release];
	[positionsArray release];
	[super dealloc];
}

@end
